<?

/** Class to handle sequential keys for any table.
 *
 *  This collection of functions (libary) is licensed under GPL2.
 *  See LICENSE or www.gnu.org for more details.
 *  @author Jyry Kuukkanen
 *
 *  $Id: soseqkey.php,v 1.7 2004/08/07 05:20:45 jyry Exp $
 */


include_once("sodebug.php");
include_once("sosqllo.php");
include_once("sosqlhi.php");
include_once("solock.php");


/* seq key table related constants */
define(SOSEQ_KEYTABLE, "soseqkeys");
define(SOSEQ_TABLECOL, "tablename");
define(SOSEQ_KEYCOL, "keycol");
define(SOSEQ_SERIECOL, "serie");
define(SOSEQ_LASTKEYCOL, "lastkey");

/** Sequential key handler class.
 *  A class to handle sequential keys. This is databse server independent.
 *  Example $seq = new soSeqKey();
 *  $seq->setDbAccess($dbaccess_see_soDa_for_more);
 *                    "owner_id", "session_id");
 *  $seq->setTarget("tablename", "keycolname", "serie");
 *  if ($seq->startUpdate()) {
 *      echo "Next key to use ".$seq->getKey()."\n";
 *      echo "Commit returns ".$seq->finishUpdate("commit")."\n";
 *  } else {
 *      echo "Update did not start at all!\n";
 *  };
 *
 *  @var string $__Table Current table to get/set key value for
 *  @var string $__KeyCol Current table key column name
 *  @var string $__Serie Current table number serie
 *  @var string $__InitKey Initial value to use with table when no key
 *      available yet
 *  @var int $__RetryCount How many times to try locking
 *  @var int $__RetryPause How many secs to wait between locking attempts
 *  @var string $__Key Current key value
 *  @var soDa $__Soda Db access object
 *  @var soLock $__Lock Seq key table rows needs locking
 *  @var string $__Owner Owner to lock rows
 *  @var string $__SessionID Session ID for locking rows
 *  @var bool $__Transact Flag to indicate when key update is in progress
 */
class soSeqKey {
    var $__Table;
    var $__KeyCol;
    var $__Serie;
    var $__InitKey = 0;
    var $__RetryCount;
    var $__RetryPause;
    var $__Key; 
    var $__Soda;
    var $__Lock;
    var $__Owner;
    var $__SessionID;
    var $__Transact = 0;


    /** constructor
     *  Class to handle sequential counters for different tables.
     *  @param string $SessionID Session ID string for seq key row locking.
     *  @param string $DbAccessStr Conection string to seq & row lock tables.
     *  @param int $RetryCount How many times to try locking seq key row.
     *  @param int $RetryPause How many seconds to wait between locking retries.
     *  @package soseqkey
     */
    function soSeqKey($DbAccessStr = "", $Owner = "", $SessionID = "",
                      $RetryCount = 5, $RetryPause = 2) {

        $this->__Soda = new soDa();
        $this->__Soda->setTable(SOSEQ_KEYTABLE);

        $this->setOwner($Owner, $SessionID);
        $this->__RetryCount = $RetryCount;
        $this->__RetryPause = $RetryPause;

        $this->__Lock = new soLock();
        $this->setDbAccess($DbAccessStr);
        soDebug("soSeqKey->constructor: exit ok", SOD_DET);
    } // constructor


    /** Sets an db connect string in place.
     *  @param string $DbAccessStr Db connection string
     *  @package soseqkey
     */
    function setDbAccess($DbAccessStr) {
        $this->__Lock->setDbAccess($DbAccessStr);
        $this->__Soda->setDbAccess($DbAccessStr);
    } //


    /** Sets owner & session id
     *  @param string $Owner @Owner
     *  @param string $SessionID Session id
     *  @package soseqkey
     */
    function setOwner($Owner, $SessionID) {
        $this->__Owner = $Owner;
        $this->__SessionID = $SessionID;
    } // setOwner


    /** Creates seq key table.
     *  @return bool Returns SQL execution return value: true(1)=ok, false(0)=not so much.
     *  @package soseqkey
     */
    function __createTable() {
        $this->__Soda->setAction(SODA_CREATE);
        $this->__Soda->setSection(SODA_DATA, SOSEQ_TABLECOL." ".
                                  SOSEQ_KEYCOL." ".
                                  SOSEQ_SERIECOL." ".
                                  SOSEQ_LASTKEYCOL." *",
                                  array("char(30)", "char(30)",
                                        "char(15)", "integer",
                                        SOSEQ_TABLECOL.",".SOSEQ_KEYCOL.",".
                                        SOSEQ_SERIECOL));
        $result = $this->__Soda->run();

        return $result;
    } // __createTable


    /** Inserts initial key value for a table.
     *  @return bool soDa result, true(1) when insert statement ran ok.
     */
    function __insertInitKey() {
        soDebug("soSeqKey->__insertInitKey ".$this->__InitKey." for ".
                $this->__Table, SOD_DET);

        $this->__Soda->setAction(SODA_INSERT);
        $this->__Soda->setSection(SODA_DATA,
                                  SOSEQ_LASTKEYCOL.":".SODT_INT." ".
                                  SOSEQ_SERIECOL." ".SOSEQ_KEYCOL." ".
                                  SOSEQ_TABLECOL,
                                  array($this->__InitKey, $this->__Serie,
                                        $this->__KeyCol, $this->__Table));

        return $this->__Soda->run();
    } // __insertInitKey


    /** Gets the current last key.
     *  @param bool $AutoSkip When set, checks lastkey from the target table
     *      and skips until first non-existing value is found. The purpose of
     *      this is simply ensure, that when seqential key is out of sync with
     *      the target table.
     *  @param bool $AutoCreate When set, tries creating the seq key table if
     *      when encountered SQL error.
     *  @return int Returns the current last key or -1 when the table
     *      is not set or no key is found for the current table.
     *  @package soseqkey
     */
    function __queryKey($AutoSkip = 1, $AutoCreate = 1) {
        if ($this->__Table == "") {
            soDebug("soSeqKey->__queryKey: null table name", SOD_HI);
            return -1; // <-- EXIT
        };

        $this->__Soda->setAction(SODA_SELECT);
        $this->__Soda->setSection(SODA_DATA, SOSEQ_LASTKEYCOL,
                                  array());
        $this->__Soda->setSection(SODA_WHERE,
                                  SOSEQ_TABLECOL." ".SOSEQ_SERIECOL,
                                  array($this->__Table, $this->__Serie));
        if ($this->__Soda->run()) {
            $result = $this->__Soda->getDataVal(SOSEQ_KEYTABLE.".".
                                                SOSEQ_LASTKEYCOL, 0, 0);
            if ($result == "") {
                if ($this->__insertInitKey()) $result = $this->__InitKey;
            };
        } else {
            /* SQL execution failed. Maybe creating the table could help? If
             * it does, then try querying again by calling this func without
             * AutoCreate feature (this if-block) */
            if (($AutoCreate) && ($this->__createTable())) {
                $result = $this->__QueryKey(0);
            } else {
                $result = NULL;
            };
        };

        if (($result != -1) && $AutoSkip) $result = $this->__checkKey($result);

        soDebug("soSeqKey->__queryKey: table ".$this->__Table.
                " resulted $result", SOD_HI);
        return $result;
    } // __queryKey


    /** Checks and skips key until it is not found in the target table.
     *  @param int $Key Key to check & skip
     *  @return int Returns next available key or -1 on error
     */
    function __checkKey($Key) {
        if ($this->__KeyCol == "") {
            soDebug("soSeqKey->__checkKey: no key column for table ".
                     $this->__Table, SOD_LO);
            return -1; // <-- EXIT
        };

        /* Table backup of current soda */
        $soda = serialize($this->__Soda);

        $count_col = $this->__Table.".count(*):int";
        $this->__Soda->setTable($this->__Table, 1);
        $this->__Soda->setSelectTablePrefix(0);
        $this->__Soda->setAction(SODA_SELECT);
        $this->__Soda->setSection(SODA_DATA, "count(*):int", array());

        /* Loop until non-existing value found or sql query error occurs */
        $result = $Key;
        $done = 0;
        while (!$done && ($result != NULL)) {
            $this->__Soda->setSection(SODA_WHERE,
                                      $this->__Table.".".$this->__KeyCol.":int",
                                      array($Key));
            if ($this->__Soda->run()) {
                if ($this->__Soda->getDataVal($count_col)) {
                    $Key++;
                } else {
                    /* No rows found for this key -> found what we wanted
                     * -> return the previous no. as it is the last used key */
                    $result = $Key -1;
                    $done = 1;
                };
            } else {
                $result = NULL;
            };
        }; // while

        /* Restore previous soda */
        $this->__Soda = unserialize($soda);

        return $result;
    } // __checkKey


    /** Locks the row for update.
     *  @param SOLM_* $Method Locking method, see soLock for details.
     *  @return SOLS_* Lock status.
     */
    function __lockRow($Method) {
        soDebug("soSeqKey->__lockRow: enter with table='".$this->__Table."', sesid='".
                $this->__SessionID."', method=$Method", SOD_DET);
        $this->__Lock->setParams(SOSEQ_KEYTABLE, array($this->__Table),
                                 $this->__Owner, $this->__SessionID);
        return $this->__Lock->execute($Method); // <-- EXIT
    } // __lockRow


    /** Starts an key update.
     *  Tries locking seq key row for current table and gets next value.
     *  @return bool Returns true(1) on success or false(0) on error, such as no
     *      table set or locking failed.
     *  @package soseqkey
     */
    function startUpdate() {
        if (($this->__Transact) ||
            ($this->__Table == "")) {
            soDebug("soSeqKey->startUpdate: transact active (".
                    $this->__Transact.") or table ('".
                    $this->__Table."') not set error -- exiting!", SOD_HI);
            return 0; // <-- EXIT
        };

        soDebug("soSeqKey->startUpdate: enter", SOD_DET);

        /* Try to get a lock for the key */
        $i = $this->__RetryCount;
        do {
            $locked = ($this->__lockRow(SOLM_NORMAL) == SOLS_OWN);
            $i--;
        } while ((!$locked) && ($i > 0));

        if (!$locked) return 0; // <-- EXIT

        /* Locked ok - next get the last key */
        $this->__Key = $this->__queryKey(1);

        /* Somehthing wrong? */
        if (soIsNull($this->__Key)) {
            soDebug("soSeqKey->startUpdate: query failed", SOD_LO);
            return 0; // <-- EXIT
        };

        /* Advance the last key to get to the next available key */
        $this->__Key++;

        $this->__Transact = 1;
        return 1;
    } // startUpdate


    /* Commits next key back to to seq key table or cancels the update.
     * Unlocks the row and if commiting, then writes the next key back to table.
     * @param string $Method Either "cancel" or "commit". Anything but
     *     "commit" == cancel.
     * @return bool True(1) on success or false(0) on error such as no
     *     transaction has been started or due to SQL executing problem.
     */
    function finishUpdate($Method = "cancel") {
        if (!$this->__Transact) return 0; // <-- EXIT

        soDebug("soSeqKey->finishUpdate: enter", SOD_DET);

        if ($Method == "commit") {
            $this->__Soda->setAction(SODA_UPDATE);
            $this->__Soda->setSection(SODA_DATA, SOSEQ_LASTKEYCOL,
                                     array($this->__Key));
            $this->__Soda->setSection(SODA_WHERE, SOSEQ_TABLECOL,
                                     array($this->__Table));
            if (!$this->__Soda->run()) return 0; // <-- EXIT
        }; // if commit

        $status = $this->__lockRow(SOLM_UNLOCK);
        if ($status != SOLS_NONE) {
            soDebug("soSeqKey: unlock failed ($status)", SOD_DET);
            return 0; // <-- EXIT
        }

        $this->__Transact = 0;
        return 1;
    } // finishUpdate


    /** Sets the target to query/update next key value for.
     *  Target consists of table name and optionally key column name and
     *  serie.
     *  @param string $TableNames Table name
     *  @param string $KeyCol Key column name in the table. This is optional,
     *      but should be used when auto skip feature is used.
     *  @param string $Serie Optional serie name when more than one number
     *      series for a table + key column.
     *  @param string $InitKey Initial key value to use and set, when no key
     *      value exists.
     *  @return bool Returns true(1) on success or false(0) on error
     *      (transaction in progress).
     *  @package soseqkey
     */
    function setTarget($TableName, $KeyCol = "", $Serie = "", $InitKey = 1) {
        if ($this->__Transact) {
            soDebug("soSeqKey->setTable: error -- transaction in progress",
                    SOD_HI);
            return 0;
        } else {
            if (($TableName == "") || (!soIsNum($InitKey))) {
                return 0; // <-- EXIT
            } else {
                $this->__Table = $TableName;
                $this->__KeyCol = $KeyCol;
                $this->__Serie = $Serie;
                $this->__InitKey = $InitKey;
            }; // if

        }; // if transact

        return 1;
    } // setTarget


    /** Sets a table name - obsolete
     *  This is for compatibility reasons only and will be removed eventually.
     *  Please use setTarget instead and modify all existing applications using
     *  this method to use setTarget instead.
     *  @param string $TableNames Table name
     *  @param string $Serie Optional serie name when more than one number
     *      series for a table.
     *  @param string $InitKey Initial key value to use and set, when no key value exists.
     *  @return bool Returns true(1) on success or false(0) on error (transaction in progress).
     *  @package soseqkey
     */
    function setTable($TableName, $Serie = "", $InitKey = 1) {
        return $this->setTarget($TableName, "", "", $InitKey);
    } // setTable


    /** Get next key for current table.
     *  Returns the next key value for current table.
     *  @return string Next key value or empty string "" on error.
     *  @package soseqkey
     */
    function getKey() {
        return $this->__Key;
    } // getKey


} // class soSeqKey

?>
